SPI原理以及SPI在Android中的实战
本文字数:13862字
预计阅读时间:35分钟
1、前言
在开题之前大家有没有这个疑惑,就是随着业务的不断发展和壮大,我们的工程结构以及代码量越来越大,交织在一起,你中有我,我 中有你,导致项目结构分工不是很明确,职责不清晰,如果线上有紧急问题需要排查的时候,我们可能就手忙脚忙了,有时候也不知道问题出现在哪个模块,导致问题排查效率低下,同时不利于项目复盘和明确职责,又或者老板想看下业务成功率,服务成功率等等一些数据的时候,我们可能为了方便直接在代码里面写了,随着业务的不断迭代,新接手的同事可能就会绞尽脑汁,这段代码到底是干什么的,为什么要写在这里(也许这段代码根本没有任何的实际业务含义,就是一个埋点或者数据统计方面的代码)等等这些问题,今天我们隆重的给大家介绍一种新的解决方案来解决这些问题,这个方案就是SPI,全称是Service Provider Interface,当然大家可能说也有别的方案,是,确实是有别的方案,条条道路通罗马,OK,我们进入正题。
2、什么是SPI
SPI(Service Provider Interface),是JDK提供的一套用来被第三方实现或者扩展的API,它是一种JVM层面的服务注册发现机制, 可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。SPI机制主要思想是将装配的控制权移到程序之外,在组件化设计中这个机制尤其重要,其核心思想就是解耦。
SPI整体机制。 Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,最核心的思想就是服务注册+服务发现 SPI和API区别。 API
为了更清楚的把这个问题讲明白,我们使用具体的图来说明SPI与API区别,上图就很清晰的说明了这两个问题
一般模块之间通信基本上都是通过接口,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口概念”。当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是API,这种接口和实现都是放在实现方的。接口和实现方属于同一个模块,密切不可分割。当接口存在于调用方这边时,就是SPI,由接口调用方确定接口规则,然后由不同的具体业务去根据这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:一个电脑制造公司,设计好了充电器标准图纸以后,那么接下来就可以把这个图纸分发给不同的厂商去生产,最后只要严格按照图纸要求,就可以生产合格的商品。通过上面的图2和图3以及配合上面的文字介绍,相信大家应该很非常清楚API和SPI的区别了。
3、SPI作用
SPI的发现能力是不需要依赖于其他类库,最重要的作用就是解耦 主要实现方式是。
java.util.ServiceLoader#load JDK自身提供的加载能力
4、实现原理
源码分析:
ServiceLoader源码
public final class ServiceLoader<S>
implements Iterable<S>
{
//配置文件所在的包目录路径
private static final String PREFIX = "META-INF/services/";
// 接口名称
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
// Android-changed: do not use legacy security code.
// private final AccessControlContext acc;
//providers就是不同实现类的缓存,key就是实现类的全限定名,value就是实现类的实例
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// //内部类LazyIterator的实例
private LazyIterator lookupIterator;
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// Android-changed: Do not use legacy security code.
// On Android, System.getSecurityManager() is always null.
// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
// Android-changed: Let the ServiceConfigurationError have a cause.
"Provider " + cn + " not found", x);
// "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
// Android-changed: Let the ServiceConfigurationError have a cause.
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service,
"Provider " + cn + " not a subtype", cce);
// fail(service,
// "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return hasNextService();
/*
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
public S next() {
// Android-changed: do not use legacy security code
/* if (acc == null) { */
return nextService();
/*
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
*/
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
/**
* Creates a new service loader for the given service type, using the
* extension class loader.
*
* <p> This convenience method simply locates the extension class loader,
* call it <tt><i>extClassLoader</i></tt>, and then returns
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>, <i>extClassLoader</i>)</pre></blockquote>
*
* <p> If the extension class loader cannot be found then the system class
* loader is used; if there is no system class loader then the bootstrap
* class loader is used.
*
* <p> This method is intended for use when only installed providers are
* desired. The resulting service will only find and load providers that
* have been installed into the current Java virtual machine; providers on
* the application's class path will be ignored.
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public static <S> S loadFromSystemProperty(final Class<S> service) {
try {
final String className = System.getProperty(service.getName());
if (className != null) {
Class<?> c = ClassLoader.getSystemClassLoader().loadClass(className);
return (S) c.newInstance();
}
return null;
} catch (Exception e) {
throw new Error(e);
}
}
// END Android-added: loadFromSystemProperty(), for internal use.
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
4.1 ServiceLoader.load加载入口,整个方法的入口是java.util.ServiceLoader#load 为入口,将当前接口Class类型及其类加载器传入至Loader变量中
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
loader.iterator() 返回一个迭代器。首先会到providers中去查找有没有存在的实例,有就直接返回,没有再到LazyIterator中查找变量传入之后,初始化类:LazyIterator,从名称就可以看出来这是一个懒加载的迭代器,只有真正使用触发时才会进行实例的,初始化,核心初始化逻辑在方法:java.util.ServiceLoader.
LazyIterator#hasNextService中
//其他代码忽略
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
//其他代码忽略
总体的实现步骤:
首先拿到配置文件名fullName 通过类加载器获得所有模块的配置文件 依次扫描每个配置文件的内容,返回配置文件内容Iterator pending,每个配置文件中可能有多个实现类的全限定名,所以pending也是个迭代器
4.2 分析nextService方法
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found", x);
}
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(
service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service,
"Provider " + cn + " not a subtype", cce);
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
首先根据nextName,Class.forName加载拿到具体实现类的class对象 Class.newInstance()实例化拿到具体实现类的实例对象 将实例对象转换service.cast为接口 返回实例对象
5、应用场景
WEB中的应用 JDBC、Spring、Dubbo、Common-Logging、Hotspot Android中的应用 在Android的组件化方案中,有一种便是通过AutoService + ServiceLoader+APT+Gradle插件的方式,自动生成META-INF/services/xxx配置文件,以实现业务Module之间的交互(跳转、传参...),具体的做法如下:
开发阶段:对关联的类使用编译期注解 定义注解处理器
apply plugin: 'java'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//要处理的注解
implementation project(path: ':annotation')
compile 'com.google.auto.service:auto-service:1.0-rc3'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc3'
//这块在gradle3.4之后有这个坑,必须加上annotationProcessor,不然生不成注解文件
compile 'com.squareup:javapoet:1.8.0'
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
编译阶段:通过注解处理器遍历查找被指定注解修饰的类的信息,收集写入到 META-INF/services 目录下的文件中。
private void generateSpiConfigFiles(){
log("generateConfigFiles start");
for(String interfaceName : servicesMap.keySet()){
String resourceFile = "META-INF/services/serviceloader/" + interfaceName;
try {
FileObject exitFile = filer.getResource(StandardLocation.CLASS_OUTPUT_PATH,"p",resourceFile);
exitFile.delete();
} catch (IOException e) {
LogUtil.d("resource file did not already exit.");
}
Map<String,String> interfaceMap = servicesMap.get(interfaceName);
if(interfaceMap == null) continue;
FileObject fileObject = null;
try {
fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT_PATH,"", resourceFile);
log("fileObject="+fileObject.toUri().toString());
Writer writer = fileObject.openWriter();
for (String key : interfaceMap.keySet()){
writer.write(key + " : " + interfaceMap.get(key));
writer.write("\n");
}
writer.flush();
writer.close();
} catch (IOException e) {
LogUtil.d("generateConfigFiles exception" + e.getStackTrace());
return;
}
}
LogUtil.d("generateSpiConfigFiles end!!!!");
}
运行阶段:通过 ServiceLoader 查找 META-INF/services 目录下指定的文件,解析文件的内容,获取要加载的类信息
6、 代码实现
项目示例截图
文字显示的这三块代表三种不同的业务场景,这里只是举个例子,抛砖引玉,大家可以根据自己的实际情况,采用serviceload开发自己的业务
SPI在Android应用架构图 实际项目开发过程中,业务模块这里都可以打包成具体的AAR,放在maven仓库,各个业务线开发是互相独立的 定义home_page接口
package com.example.api;
public interface IHome {
String show();
}
home_page_api module build.gradle文件配置
定义home_page服务
创建home_page module
package com.example.module;
import com.example.api.IHome;
public class HomeService implements IHome {
private static final String TAG = HomeService.class.getSimpleName();
@Override
public String show() {
return "I am HomeService ";
}
}
home_page module build.gradle配置
在壳子工程app 使用
build.gradle中依赖各个服务和接口module
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation project(path: ':home_page_module')
implementation project(path: ':home_page_module_api')
implementation project(path: ':login_module')
implementation project(path: ':login_module_api')
implementation project(path: ':order_module_api')
implementation project(path: ':order_module')
}
APP壳子集成使用
按照上述的步骤就可以实现业务解耦,达到高内聚,低耦合的标准,同时大大提高项目的可维护性,APP壳子是非常轻量级的一层,只做最基本的集成
职责,或者是一些flavors的配置,启动数据等等的基本配置信息,配合上 flavors那么就可以一次打包生成不同的产物,根据不同的flavors替换不同的
服务,各个业务线去完成具体的业务逻辑。
最后附上源码地址:
https://github.com/zhaoqiang1991/providerdemo
7、FAQ
SPI优点: spi技术适合在一些大型的项目中,能够大大地提高接口设计的灵活性,解耦,高内聚,提升了团队协作的效率,明确了业务边界 SPI缺点
(1):spi 不适合小型项目,因为会多出来很多的module,同时项目架构比较复杂,无疑增加了代码阅读的难度
(2):遍历加载所有的实现类,效率还是相对较低的(初次启动的时候,加载过以后缓存起来);
(3):当多个 ServiceLoader 同时 load 时,会有并发问题。
综上所述:一个技术是不是适合所在的项目,要具体情况具体分析,不是说那个技术好就适用于本项目。
参考:
https://blog.csdn.net/ecjtuhq/article/details/107552479
https://zhuanlan.zhihu.com/p/436560515
结束,感谢阅读!
也许你还想看
(▼点击文章标题或封面查看)
2022-09-15
2022-09-08
2022-08-25
2022-08-18
2022-08-11